package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * 1、缓存穿透：客户端请求的数据在缓存和数据库中都不存在，这样缓存永远不会生效，这些请求都会打到数据库<br/>
 * 解决方案：(1)缓存空对象<br/>
 *         (2)布隆过滤<br/>
 * 2、缓存雪崩是指在通义时段大量缓存key同时失效或redis服务宕机，导致大量请求到达数据库，带来巨大压力<br/>
 * 解决方案：(1)给不同的key的TTL添加随机值<br/>
 *         (2)利用redis集群提高服务的可用性<br/>
 *         (3)给缓存业务添加降级限流策略<br/>
 *         (4)给业务添加多级缓存<br/>
 * 3、缓存击穿问题也叫热点key问题，就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了，无数请求的访问会瞬间给数据库带来强大冲击。<br/>
 * 解决方案：(1)互斥锁：某一线程查询缓存未命中，则尝试获取互斥锁，获取锁成功，则再次查询缓存，命中则返回，未命中则进行缓存重建，最后释放锁并返回；
 *             获取锁失败，则等待一段时间后重新查询redis缓存、尝试获取锁...<br/>
 *             (优点: 没有额外的内存消耗、保证一致性、实现简单;<br/>
 *             缺点: 缓存重建业务较复杂，且重建时其他线程只能等待，影响性能、可能有死锁的风险)<br/>
 *         (2)逻辑过期：提前写入缓存并设置逻辑过期时间，不设置 TTL，某一线程查询缓存命中但发现已过期，则尝试获取互斥锁，获取锁成功，则再次查询缓存，命中且未过期则返回，
 *            过期则(利用线程池)开启一段新线程进行缓存重建，后释放锁，原来的线程则直接返回过期数据；
 *            获取锁失败，则直接返回过期数据。<br/>
 *            (优点: 线程无需等待，性能好;<br/>
 *            缺点: 不保证一致性、有额外内存消耗、实现复杂)<br/>
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheClient {
    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 存值
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 存值，逻辑过期
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 缓存穿透解决方案(防止通过不断查询不存在的值恶意攻击数据库)
     * @param keyPrefix 缓存数据的键的前缀
     * @param id 类型定位泛型
     * @param type 结果类型
     * @param dbFallback <参数类型, 返回值类型>数据库查询操作函数
     * @param time TTL
     * @param unit TTL单位
     * @return 返回泛型对象
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){

        String key = keyPrefix+id;
        //1、从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isNotBlank(json)){
            //3、存在，直接返回
            return JSONUtil.toBean(json, type);
        }
        //判断是否为空值（解决缓存穿透————不断查询不存在的值）
        if(json != null){
            return  null;
        }

        //4、不存在，根据id查询数据库
        R r = dbFallback.apply(id);
        //5、不存在，返回空
        if(r == null){
            //向redis中写入空值（解决缓存穿透————不断查询不存在的值）
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //6、存在，写入redis
        set(key,r,time, unit);
        //7、返回
        return r;
    }

    /**
     * 使用互斥锁解决缓存击穿(防止在高并发的情况下查询数据库导致的数据库崩溃)
     * @param keyPrefix
     * @param LockKeyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param nullTime
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithMutex(String keyPrefix, String LockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, Long nullTime, TimeUnit unit){

        String key = keyPrefix+id;
        String LockKey = LockKeyPrefix+id;
        R r = null;
        //1、从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isNotBlank(json)){
            //3、存在，直接返回
            return JSONUtil.toBean(json, type);
        }
        //4、判断是否为空值（解决缓存穿透————不断查询不存在的值）
        if(json != null){
            return  null;
        }

        try {
            //5、未命中，实现缓存重建
            //5.1、尝试获取互斥锁
            boolean isLock = tryLock(LockKey);
            if(!isLock){
                //5.2、获取锁失败，休眠后再次尝试获取互斥锁
                Thread.sleep(50);
                queryWithMutex(keyPrefix, LockKeyPrefix, id, type, dbFallback, time, nullTime, unit);
            }
            //5.3、获取互斥锁成功，再次检测redis缓存是否存在
            json = stringRedisTemplate.opsForValue().get(key);
            if(StrUtil.isNotBlank(json)){
                unLock(LockKey);
                return JSONUtil.toBean(json, type);
            }

            //6、不存在，根据id查询数据库
            r = dbFallback.apply(id);
            // TODO 模拟重建延时
            Thread.sleep(200);
            //7、不存在，返回空
            if(r == null){
                //向redis中写入空值（解决缓存穿透————不断查询不存在的值）
                set(key, "", nullTime, unit);
                return null;
            }
            //6、存在，写入redis
            String jsonStr = JSONUtil.toJsonStr(r);
            set(key, jsonStr, time, unit);
        }
        catch (Exception e){
            throw new RuntimeException(e);
        }
        finally {
            //7、释放互斥锁
            unLock(LockKey);
        }
        //8、返回
        return r;
    }

    //创建一个十个线程的线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 使用逻辑过期解决缓存击穿(防止在高并发的情况下查询数据库导致的数据库崩溃)
     * @param keyPrefix
     * @param LockKeyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param expireSeconds
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, String LockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireSeconds, TimeUnit unit){

        String key = keyPrefix+id;
        String LockKey = LockKeyPrefix+id;
        //1、从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if(StrUtil.isBlank(json)){
            //3、不存在，直接返回null
            return null;
        }
        //4、命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        R r = JSONUtil.toBean(data, type);
        //5、判断是否过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if(expireTime.isAfter(LocalDateTime.now())){
            //5.1、未过期，直接返回对象
            return r;
        }
        //6、过期，需要缓存重建
        //6.1、获取互斥锁
        boolean isLock = tryLock(LockKey);
        if(isLock){
            //6.2、获取互斥锁成功，开启独立线程，实现缓存重建
            //6.3、再次检测redis缓存是否过期
            json = stringRedisTemplate.opsForValue().get(key);
            //判断是否存在
            if(StrUtil.isBlank(json)){
                //不存在，直接返回null
                return null;
            }
            //命中，需要先把json反序列化为对象
            redisData = JSONUtil.toBean(json, RedisData.class);
            data = (JSONObject)redisData.getData();
            r = JSONUtil.toBean(data, type);
            //判断是否过期
            expireTime = redisData.getExpireTime();
            if(expireTime.isAfter(LocalDateTime.now())){
                //未过期，直接返回对象
                unLock(LockKey);
                return r;
            }
            // 过期，重建缓存
            // 实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    saveToRedis(keyPrefix, id, dbFallback, expireSeconds, unit);
                }
                catch (Exception e){
                    throw new RuntimeException(e);
                }
                finally {
                    //释放锁
                    unLock(LockKey);
                }
            });
        }
        //6.3、返回过期的商铺信息
        return r;
    }

    /**
     * 尝试获取互斥锁
     * @param key
     * @return
     */
    public boolean tryLock(String key){
        Boolean rock = stringRedisTemplate.opsForValue().setIfAbsent(key, "rock", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(rock);
    }

    /**
     * 释放互斥锁
     * @param key
     */
    public void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    public <R, ID> void saveToRedis(String keyPrefix, ID id, Function<ID, R> dbFallback, Long expireSeconds, TimeUnit unit) throws InterruptedException {
        // 1、查询店铺数据
        R r = dbFallback.apply(id);
        // TODO 模拟重建延时
        Thread.sleep(200);
        // 2、封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(r);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3、写入redis
        setWithLogicalExpire(keyPrefix+id, JSONUtil.toJsonStr(redisData), expireSeconds, unit);
    }
}





















